package net.avcompris.binding.impl;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static net.avcompris.binding.impl.AbstractBinderInvocationHandler.getAbstractBinderInvocationHandler;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.binding.Binder;
import net.avcompris.binding.Binding;
import net.avcompris.binding.ClassBinding;
import net.avcompris.binding.MethodBinding;
import net.avcompris.binding.annotation.Nodes;
import net.avcompris.binding.annotation.XPath;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;
import com.google.common.collect.ImmutableSet;

/**
 * superclass for implementations of {@link Binder}: Only the
 * {@link #bindInterface(Object, ClassLoader, Class, BindConfiguration))} method
 * remains to be implemented.
 * 
 * @author David Andrianavalontsalama
 */
public abstract class AbstractBinder<U> implements Binder<U> {

	protected final void setThis(final Binder<U> binder) {

		this.thisBinder = nonNullArgument(binder, "binder");
	}

	protected final Binder<U> getThis() {

		return thisBinder;
	}

	private Binder<U> thisBinder = this;

	@Override
	public final <T> T bind(final U node, final Class<T> clazz) {

		nonNullArgument(clazz, "clazz");

		return bind(node, getDefaultClassLoader(clazz), clazz);
	}

	/**
	 * get a default "convenient" class loader... That is because
	 * <tt>clazz.getClassLoader()</tt> is not always useful: For instance
	 * {@link String}<tt>.class.getClassLoader()</tt> returns <tt>null</tt>.
	 */
	private static ClassLoader getDefaultClassLoader(final Class<?> clazz) {

		nonNullArgument(clazz, "clazz");

		final ClassLoader classLoader = clazz.getClassLoader();

		if (classLoader != null) {

			return classLoader;
		}

		return Thread.currentThread().getContextClassLoader();
	}

	@Override
	public final <T> T bind(final BindConfiguration configuration,
			final U node, final Class<T> clazz) {

		nonNullArgument(clazz, "clazz");

		return bind(configuration, node, getDefaultClassLoader(clazz), clazz);
	}

	@Override
	public final <T> T bind(final U node, final ClassLoader classLoader,
			final Class<T> clazz) {

		final XPath xpathAnnotation = clazz.getAnnotation(XPath.class);

		final Nodes nodesAnnotation = clazz.getAnnotation(Nodes.class);

		final BindConfiguration configuration = BindConfiguration.newBuilder()
				.setXPath(xpathAnnotation).setNodes(nodesAnnotation).build();

		return bind(configuration, node, classLoader, clazz);
	}

	@Override
	public final <T> T bind(final BindConfiguration configuration,
			final U node, final ClassLoader classLoader, final Class<T> clazz) {

		nonNullArgument(clazz, "clazz");

		if (String.class.equals(clazz)) {

			final BindConfiguration c = configuration.hasXPath() ? configuration
					: BindConfiguration.newBuilder(configuration)
							.setXPath(clazz.getAnnotation(XPath.class)).build();

			final Dummy dummy = bind(c, node, classLoader, Dummy.class);

			return clazz.cast(dummy.getStringValue());

		} else if (int.class.equals(clazz)) {

			final BindConfiguration c = configuration.hasXPath() ? configuration
					: BindConfiguration.newBuilder(configuration)
							.setXPath(clazz.getAnnotation(XPath.class)).build();

			final Dummy dummy = bind(c, node, classLoader, Dummy.class);

			@SuppressWarnings("unchecked")
			final T intValue = (T) Integer.class.cast(dummy.getIntValue());

			return intValue;

		} else if (clazz.isPrimitive()) {

			throw new NotImplementedException("clazz: " + clazz.getName());

		} else if (clazz.isInterface()) {

			final T instance = bindInterface(configuration, node, classLoader,
					clazz);

			final InstanceUpdater<U> invocationHandler = getAbstractBinderInvocationHandler(
			// Proxy.getInvocationHandler(
					instance, node);

			invocationHandler.setInstance(instance);

			if (Binding.class.isAssignableFrom(clazz)) {

				return BindingSpecifics.<T, U> newProxy(instance, node,
						classLoader, clazz);

			} else {

				return instance;
			}

		} else {

			throw new IllegalArgumentException("Clazz should be an interface: "
					+ clazz);
		}
	}

	protected abstract <T> T bindInterface(BindConfiguration configuration,
			U node, ClassLoader classLoader, Class<T> clazz);

	// @Override
	// public final <T> T bind(final U node, final ClassLoader classLoader,
	// final Class<T> clazz, final BindConfiguration configuration) {
	//
	// nonNullArgument(clazz, "clazz");
	//
	// final XPath xpathAnnotation = clazz.getAnnotation(XPath.class);
	//
	// if (xpathAnnotation == null) {
	//
	// throw new IllegalArgumentException(
	// "Class should be annotated with @XPath: " + clazz.getName());
	// }
	//
	// final String xpath = xpathAnnotation.value();
	//
	// return bind(node, xpath, classLoader, clazz, configuration);
	// }

	// private static BindConfiguration loadConfiguration(final Class<?> clazz)
	// {
	//
	// nonNullArgument(clazz, "clazz");
	//
	// final XPathConfiguration xpathConfiguration = clazz
	// .getAnnotation(XPathConfiguration.class);
	//
	// final boolean elementsEverywhere = (xpathConfiguration == null) ? false
	// : xpathConfiguration.elementsEverywhere();
	// final boolean emptyAttributes = (xpathConfiguration == null) ? false
	// : xpathConfiguration.emptyAttributes();
	//
	// return new BindConfiguration(elementsEverywhere, emptyAttributes);
	// }

	private final Map<String, ClassBinding> classBindings = new HashMap<String, ClassBinding>();

	@Override
	public final void addClassBinding(
			@Nullable final BindConfiguration configuration) {

		addClassBinding(new ClassBinding(configuration));
	}

	@Override
	public final void addClassBinding(
			@Nullable final BindConfiguration configuration,
			final Class<?> clazz) {

		addClassBinding(new ClassBinding(configuration, clazz));
	}

	@Override
	public final void addClassBinding(
			@Nullable final BindConfiguration configuration,
			final String className) {

		addClassBinding(new ClassBinding(configuration, className));
	}

	@Override
	public final synchronized void addClassBinding(final ClassBinding binding) {

		nonNullArgument(binding, "binding");

		final String key = binding.getBindingClassName();

		if ("*".equals(key)) {

			classBindings.clear();
		}

		if (binding.getConfiguration() == null) {

			classBindings.remove(key);

		} else {

			classBindings.put(key, binding);
		}
	}

	@Override
	public final ImmutableSet<ClassBinding> getClassBindings() {

		return ImmutableSet.copyOf(classBindings.values());
	}

	private final Map<String, MethodBinding> methodBindings = new HashMap<String, MethodBinding>();

	@Override
	public final void addMethodBinding(
			@Nullable final BindConfiguration configuration, final Method method) {

		addMethodBinding(new MethodBinding(configuration, method));
	}

	@Override
	public final void addMethodBinding(
			@Nullable final BindConfiguration configuration,
			final Class<?> clazz, final String methodName,
			final Class<?>... paramTypes) {

		addMethodBinding(new MethodBinding(configuration, clazz, methodName,
				paramTypes));
	}

	@Override
	public final void addMethodBinding(
			@Nullable final BindConfiguration configuration,
			final String className, final String methodName,
			final Class<?>... paramTypes) {

		addMethodBinding(new MethodBinding(configuration, className,
				methodName, paramTypes));
	}

	@Override
	public final synchronized void addMethodBinding(final MethodBinding binding) {

		nonNullArgument(binding, "binding");

		final String key;

		final String className = binding.getBindingClassName();
		final String methodName = binding.getMethodName();

		final StringBuilder sb = new StringBuilder().append(className)
				.append('#').append(methodName).append('(');

		boolean start = true;

		for (final Class<?> paramType : binding.getParamerTypes()) {

			if (start) {

				start = false;

			} else {

				sb.append(", ");
			}

			sb.append(paramType.getName());
		}

		sb.append(')');

		key = sb.toString();

		if (key.startsWith("*#*(")) { // all classes, all method names

			methodBindings.clear();

		} else if (key.startsWith("*#")) { // all classes, a given method name

			final List<String> keysToBeRemoved = new ArrayList<String>();

			final String sToBeRemoved = "#" + methodName + "(";

			for (final String k : methodBindings.keySet()) {

				if (k.contains(sToBeRemoved)) {

					keysToBeRemoved.add(k);
				}
			}

			for (final String k : keysToBeRemoved) {

				methodBindings.remove(k);
			}

		} else if (key.contains("#*(")) { // a given class, all method names

			final List<String> keysToBeRemoved = new ArrayList<String>();

			final String sToBeRemoved = className + "#";

			for (final String k : methodBindings.keySet()) {

				if (k.contains(sToBeRemoved)) {

					keysToBeRemoved.add(k);
				}
			}

			for (final String k : keysToBeRemoved) {

				methodBindings.remove(k);
			}
		}

		if (binding.getConfiguration() == null) {

			methodBindings.remove(key);

		} else {

			methodBindings.put(key, binding);
		}
	}

	@Override
	public final void addMethodBinding(
			@Nullable final BindConfiguration configuration,
			final Class<?> clazz, final Class<?>... paramTypes) {

		addMethodBinding(configuration, clazz, "*", paramTypes);
	}

	@Override
	public final void addMethodBinding(
			@Nullable final BindConfiguration configuration,
			final String methodName, final Class<?>... paramTypes) {

		addMethodBinding(configuration, "*", methodName, paramTypes);
	}

	@Override
	public final ImmutableSet<MethodBinding> getMethodBindings() {

		return ImmutableSet.copyOf(methodBindings.values());
	}

	@Override
	public final void addFieldBinding(
			@Nullable final BindConfiguration configuration,
			final Class<?> clazz, final String field) {

		throw new NotImplementedException();
	}

	/**
	 * utility method to add bindings for methods in a given class with names
	 * <tt>getXxx()</tt>, <tt>isNullXxx()</tt> and <tt>sizeOfXxx()</tt>.
	 */
	@Override
	public final void addFieldBinding(
			@Nullable final BindConfiguration configuration,
			final String className, final String field) {

		throw new NotImplementedException();
	}

	/**
	 * utility method to add bindings for methods with names <tt>getXxx()</tt>,
	 * <tt>isNullXxx()</tt> and <tt>sizeOfXxx()</tt> in all classes:
	 * <tt>className</tt> is <tt>"*"</tt>.
	 */
	@Override
	public final void addFieldBinding(
			@Nullable final BindConfiguration configuration, final String field) {

		addFieldBinding(configuration, "*", field);
	}
}
